<?php

namespace Vhall\ErrorHandler;

use Symfony\Component\ErrorHandler\BufferingLogger;
use Symfony\Component\ErrorHandler\DebugClassLoader;
use Symfony\Component\ErrorHandler\ErrorHandler as SymfonyErrorHandler;
use Throwable;
use function in_array;
use const FILTER_VALIDATE_BOOLEAN;
use const PHP_SAPI;

class ErrorHandler
{
    /**
     * @var ExceptionHandler
     */
    private $exceptionHandler;

    /**
     * Bootstrap the given application.
     *
     * @param bool $debug
     * @return ErrorHandler
     */
    public static function register(bool $debug = false): ErrorHandler
    {
        error_reporting(-1);

        if (!in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) {
            ini_set('display_errors', 0);
        } elseif (!filter_var(ini_get('log_errors'), FILTER_VALIDATE_BOOLEAN) || ini_get('error_log')) {
            // CLI - display errors only if they're not already logged to STDERR
            ini_set('display_errors', 1);
        }

        @ini_set('zend.assertions', 1);
        ini_set('assert.active', 1);
        ini_set('assert.warning', 0);
        ini_set('assert.exception', 1);

        DebugClassLoader::enable();
        $obj = new self;
        SymfonyErrorHandler::register(new SymfonyErrorHandler(new BufferingLogger(), $debug))
            ->setExceptionHandler([$obj, 'renderException']);
        return $obj;
    }

    /**
     * Handle an uncaught exception from the application.
     *
     * Note: Most exceptions can be handled via the try / catch block in
     * the HTTP and Console kernels. But, fatal error exceptions must
     * be handled differently since they are not normal exceptions.
     *
     * @param Throwable $e
     * @return void
     * @throws Throwable
     */
    public function renderException(Throwable $e): void
    {
        if ($phpHandler = set_exception_handler([$this, 'renderException'])) {
            restore_exception_handler();
            if (!\is_array($phpHandler) || !$phpHandler[0] instanceof SymfonyErrorHandler) {
                $errorHandler = true;
            } elseif ($errorHandler = $phpHandler[0]->setExceptionHandler([$this, 'renderException'])) {
                $phpHandler[0]->setExceptionHandler($errorHandler);
            }
        }
        try {
            $this->getExceptionHandler()->report($e);
        } catch (Throwable $e) {
            //
        } finally {
            if (!$phpHandler) {
                if (set_exception_handler([$this, 'renderException']) === [$this, 'renderException']) {
                    restore_exception_handler();
                }
                restore_exception_handler();
            } elseif (!$errorHandler) {
                $finalHandler = $phpHandler[0]->setExceptionHandler(null);
                if ($finalHandler !== [$this, 'renderException']) {
                    $phpHandler[0]->setExceptionHandler($finalHandler);
                }
            }
        }
        if (!$this->runningInConsole()) {
            $this->getExceptionHandler()->renderHttpResponse($e);
            return;
        }
        $this->getExceptionHandler()->renderForConsole($e);
    }

    /**
     * Determine if the application is running in the console.
     *
     * @return bool
     */
    public function runningInConsole()
    {
        return in_array(PHP_SAPI, ['cli', 'phpdbg'], true);
    }

    /**
     * Get an instance of the exception handler.
     * @return ExceptionHandler
     */
    public function getExceptionHandler()
    {
        if ($this->exceptionHandler) {
            return $this->exceptionHandler;
        }
        $this->exceptionHandler = new Handler;
        return $this->exceptionHandler;
    }

    /**
     * Set an instance of the exception handler.
     */
    public function setExceptionHandler(ExceptionHandler $exceptionHandler): void
    {
        $this->exceptionHandler = $exceptionHandler;
    }
}
